import { useState, useLayoutEffect, useRef, useDebugValue, useEffect } from 'react';

/*
 * 提供了状态再不定层级关系中共享和同步更新的机制
 * A) 比 react 自带的 context 更方便
 * 1. 不增加 <Provider></Provider> 层级，提供共享数据源的组件可以 hook 方式提供
 * 2. 一个 FC 可以提供任意多套共享数据，<Provide> 方式会增加多个层级
 * 3. 无需再共享状态组件的公共父组件上 provide，不受组件数层级限制，任意位置组件可以和任意其他位置组件共享状态
 * B) 比 redux 更有优势
 * 1. 按照 namespace/path 分隔共享区域，各区域隔离，不占全局空间，无不必要的耦合
 * 2. 订阅通知性能极高，都是按path注册订阅的
 * todo
 * 1. 提供 state 比较函数，默认按引用，可以提供浅比较，这样方便一个 path 提供多个共享数据子项
 *    当然，多个共享数据子项也完全可以通过多个 useProvide 提供
 *
 */

const store = new Map<string, any>();
type cb = React.Dispatch<any>;
const pathToListener = new Map<string, cb[]>();

function getAndMakeListener(path: string) {
  const listeners = pathToListener.get(path);
  if (!listeners) {
    const list = [] as cb[];
    pathToListener.set(path, list); // 注册空监听者
    return list;
  }
  return listeners;
}

function getStateByPath(path: string) {
  return store.get(path);
}

/**
 * 提供共享数据源及其变更通知
 * @param initState
 */
export function useProvide(path: string, state: any, keep: boolean = true) {
  useDebugValue(path);
  const ref = useRef(state);
  // 需要马上注册，因后面 useInject 可能马上用到
  useLayoutEffect(() => {
    ref.current = state;
    const listeners = getAndMakeListener(path);
    listeners.forEach(setState => setState(state));
    store.set(path, state);
  }, [state]);
  useEffect(() => {
    return () => {
      if (keep) return;
      // console.log('useProvide not keep clean', path);
      const listeners = getAndMakeListener(path);
      listeners.forEach(setState => setState(undefined));
      store.delete(path);
    }
  }, []);
}

/**
 * 根据 path 返回共享数据源，变化后将返回更新的共享状态
 * @param path 共享数据源的访问路径或命名空间
 */
export function useInject(path: string, defaultProducer: any = () => {}) {
  useDebugValue(path);
  const [state, setState] = useState(() => getStateByPath(path) || defaultProducer());
  // 组件退出，取消订阅
  useLayoutEffect(() => {
    setState(getStateByPath(path));
    getAndMakeListener(path).push(setState); // 上来就订阅，不等
    return () => {
      const listeners = getAndMakeListener(path);
      const index = listeners.findIndex(cb => cb === setState);
      listeners.splice(index, 1);
    };
  }, []);
  return [state];
}
